package com.three.service;

import com.google.common.base.Strings;
import com.three.api.connection.SessionContext;
import com.three.api.event.PlayerOfflineEvent;
import com.three.api.event.PlayerOnlineEvent;
import com.three.api.spi.Spi;
import com.three.api.spi.handler.BindValidator;
import com.three.api.spi.handler.BindValidatorFactory;
import com.three.api.spi.net.DnsMapping;
import com.three.api.spi.net.DnsMappingManager;
import com.three.common.message.base.*;
import com.three.common.router.RemoteRouter;
import com.three.common.router.RemoteRouterManager;
import com.three.common.security.AesCipher;
import com.three.common.security.CipherBox;
import com.three.config.LanguageConfig;
import com.three.constant.LanguageId;
import com.three.domain.account.AccountPo;
import com.three.domain.player.PlayerPo;
import com.three.event.EventBus;
import com.three.netty.core.router.LocalRouter;
import com.three.netty.core.router.LocalRouterManager;
import com.three.netty.core.router.RouterCenter;
import com.three.netty.core.session.ReusableSession;
import com.three.netty.core.session.ReusableSessionManager;
import com.three.netty.http.HttpCallback;
import com.three.netty.http.HttpClient;
import com.three.netty.http.NettyHttpClient;
import com.three.netty.http.RequestContext;
import com.three.repository.AccountRepository;
import com.three.repository.PlayerRepository;
import com.three.tools.common.Profiler;
import com.three.tools.config.ConfigManager;
import com.three.utils.LogUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

/**
 * Created by mathua on 2017/6/7.
 */
@Service
public class BaseModuleService {

    private BindValidator validator = BindValidatorFactory.create();
    private static final Logger LOGGER = LoggerFactory.getLogger(BaseModuleService.class);
    private final HttpClient httpClient = NettyHttpClient.I();
    private final DnsMappingManager dnsMappingManager = DnsMappingManager.create();

    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private PlayerRepository playerRepository;

    public void doSecurity(LoginReqMessage message) {
        byte[] iv = message.getIv();//AES密钥向量16位
        byte[] clientKey = message.getClientKey();//客户端随机数16位
        byte[] serverKey = CipherBox.I.randomAESKey();//服务端随机数16位
        byte[] sessionKey = CipherBox.I.mixKey(clientKey, serverKey);//会话密钥16位

        //1.校验客户端消息字段
        if (Strings.isNullOrEmpty(message.getDeviceId())
                || iv.length != CipherBox.I.getAesKeyLength()
                || clientKey.length != CipherBox.I.getAesKeyLength()) {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.PARAM_INVALID)).close();
            LogUtils.CONN.error("handshake failure, message={}, conn={}", message, message.getConnection());
            return;
        }

        //2.重复握手判断
        SessionContext context = message.getConnection().getSessionContext();
        if (message.getDeviceId().equals(context.getDeviceId())) {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.REPEAT_LOGIN)).send();
            LogUtils.CONN.warn("handshake failure, repeat handshake, conn={}", message.getConnection());
            return;
        }

        //3.更换会话密钥RSA=>AES(clientKey)
        context.changeCipher(new AesCipher(clientKey, iv));

        //4.生成可复用session, 用于快速重连
        ReusableSession session = ReusableSessionManager.I.genSession(context);

        //5.计算心跳时间
        int heartbeat = ConfigManager.I.getHeartbeat();

        //6.响应握手成功消息
        LoginRespMessage
                .from(message)
                .setHeartbeat(heartbeat)
                .send();

        //7.更换会话密钥AES(clientKey)=>AES(sessionKey)
        context.changeCipher(new AesCipher(sessionKey, iv));

        //8.保存client信息到当前连接
        context.setOpenId(message.getOpenId())
                .setOsName(message.getOsName())
                .setOsVersion(message.getOsVersion())
                .setClientVersion(message.getClientVersion())
                .setDeviceId(message.getDeviceId())
                .setHeartbeat(heartbeat);

        //9.保存可复用session到Redis, 用于快速重连
        ReusableSessionManager.I.cacheSession(session);

        // 10.如果没有账号，则创建一个新账号
        List<AccountPo> accountPoList = accountRepository.findByOpenId(message.getOpenId());
        if(accountPoList == null || accountPoList.isEmpty()) {
            String ip = message.getConnection().getChannel().localAddress().toString();
            try {
                PlayerPo playerPo = PlayerPo.createPlayer(message.getOpenId());
                playerPo = playerRepository.save(playerPo);
                AccountPo accountPo = AccountPo.createAccount(playerPo.getPlayerId(), message.getOpenId(), message.getPlatform(), ip, message.getOsName() + message.getOsVersion());
                accountRepository.save(accountPo);
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
        LogUtils.CONN.info("handshake success, message = {}, conn={}", message, message.getConnection());
        login(message);
    }

    public void doInsecurity(LoginReqMessage message) {

        //1.校验客户端消息字段
        if (Strings.isNullOrEmpty(message.getDeviceId())) {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.PARAM_INVALID)).close();
            LogUtils.CONN.error("handshake failure, message={}, conn={}", message, message.getConnection());
            return;
        }

        //2.重复握手判断
        SessionContext context = message.getConnection().getSessionContext();
        if (message.getDeviceId().equals(context.getDeviceId())) {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.REPEAT_LOGIN)).send();
            LogUtils.CONN.warn("handshake failure, repeat handshake, conn={}", message.getConnection());
            return;
        }

        //3.响应握手成功消息
        int heartbeat = ConfigManager.I.getHeartbeat();
        LoginRespMessage
                .from(message)
                .setHeartbeat(heartbeat)
                .send();

        //4.保存client信息到当前连接
        context.setOpenId(message.getOpenId())
                .setOsName(message.getOsName())
                .setOsVersion(message.getOsVersion())
                .setClientVersion(message.getClientVersion())
                .setDeviceId(message.getDeviceId())
                .setHeartbeat(Integer.MAX_VALUE);

        // 5.如果没有账号，则创建一个新账号
        List<AccountPo> accountPoList = accountRepository.findByOpenId(message.getOpenId());
        if(accountPoList == null || accountPoList.isEmpty()) {
            String ip = message.getConnection().getChannel().localAddress().toString();
            try {
                PlayerPo playerPo = PlayerPo.createPlayer(message.getOpenId());
                playerPo = playerRepository.save(playerPo);
                AccountPo accountPo = AccountPo.createAccount(playerPo.getPlayerId(), message.getOpenId(), message.getPlatform(), ip, message.getOsName() + message.getOsVersion());
                accountRepository.save(accountPo);
            } catch(Exception e) {
                e.printStackTrace();
            }
        }

        LogUtils.CONN.info("handshake success, conn={}", message.getConnection());
        login(message);
    }

    private void login(LoginReqMessage message) {
        //1.绑定用户时先看下是否握手成功
        SessionContext context = message.getConnection().getSessionContext();
        if (context.handshakeOk()) {
            //处理重复绑定问题
            if (context.getPlayerId() != 0) {
//                OkMessage.from(message).setMsg(LanguageConfig.getText(LanguageId.LOGIN_SUCCESS)).sendRaw();
                LogUtils.CONN.info("login player success, openId={}, session={}", context.getOpenId(), context);
                return;
            }
            List<AccountPo> accountPoList = accountRepository.findByOpenId(context.getOpenId());
            AccountPo account = accountPoList.get(0);
            //2.如果握手成功，就把用户链接信息注册到路由中心，本地和远程各一份
            boolean success = validator.validate(account.getPlayerId()) && RouterCenter.I.register(account.getPlayerId(), message.getConnection());
            if (success) {
                context.setPlayerId(account.getPlayerId());
                EventBus.I.post(new PlayerOnlineEvent(message.getConnection(), account.getPlayerId(),account.getLoginTime()));
//                OkMessage.from(message).setMsg(LanguageConfig.getText(LanguageId.LOGIN_SUCCESS)).sendRaw();
                LogUtils.CONN.info("login player success, playerId={}, session={}", account.getPlayerId(), context);

                // 登录成功更新
                String ip = message.getConnection().getChannel().localAddress().toString();
                account.updateLoginInfo(ip, message.getOsName() + message.getOsVersion());
            } else {
                //3.注册失败再处理下，防止本地注册成功，远程注册失败的情况，只有都成功了才叫成功
                RouterCenter.I.unRegister(account.getPlayerId(), context.getClientType());
                ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.LOGIN_FAILED)).close();
                LogUtils.CONN.info("login player failure, playerId={}, session={}", account.getPlayerId(), context);
            }
        } else {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.NOT_LOGIN)).close();
            LogUtils.CONN.error("login player failure not handshake, conn={}", message.getConnection());
        }
    }

    /**
     * 目前是以用户维度来存储路由信息的，所以在删除路由信息时要判断下是否是同一个设备
     * 后续可以修改为按设备来存储路由信息。
     *
     * @param message
     */
    public void logout(LogoutMessage message) {
        SessionContext context = message.getConnection().getSessionContext();
        if (context.getPlayerId() != 0) {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.INVALID_ACTION)).close();
            LogUtils.CONN.error("logout player failure invalid param, session={}", context);
            return;
        }
        //1.解绑用户时先看下是否握手成功
        if (context.handshakeOk()) {
            //2.先删除远程路由, 必须是同一个设备才允许解绑
            boolean unRegisterSuccess = true;
            int clientType = context.getClientType();
            long playerId = context.getPlayerId();
            RemoteRouterManager remoteRouterManager = RouterCenter.I.getRemoteRouterManager();
            RemoteRouter remoteRouter = remoteRouterManager.lookup(playerId, clientType);
            if (remoteRouter != null) {
                String deviceId = remoteRouter.getRouteValue().getDeviceId();
                if (context.getDeviceId().equals(deviceId)) {//判断是否是同一个设备
                    unRegisterSuccess = remoteRouterManager.unRegister(playerId, clientType);
                }
            }
            //3.删除本地路由信息
            LocalRouterManager localRouterManager = RouterCenter.I.getLocalRouterManager();
            LocalRouter localRouter = localRouterManager.lookup(playerId, clientType);
            if (localRouter != null) {
                String deviceId = localRouter.getRouteValue().getSessionContext().getDeviceId();
                if (context.getDeviceId().equals(deviceId)) {//判断是否是同一个设备
                    unRegisterSuccess = localRouterManager.unRegister(playerId, clientType) && unRegisterSuccess;
                }
            }

            //4.路由删除成功，广播用户下线事件
            if (unRegisterSuccess) {
                context.setPlayerId(0);
                EventBus.I.post(new PlayerOfflineEvent(message.getConnection(), playerId));
                OkMessage.from(message).setMsg(LanguageConfig.getText(LanguageId.LOGOUT_SUCCESS)).sendRaw();
                LogUtils.CONN.info("logout player success, playerId={}, session={}", playerId, context);
            } else {
                ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.LOGOUT_FAILED)).sendRaw();
                LogUtils.CONN.error("logout player failure, unRegister router failure, playerId={}, session={}", playerId, context);
            }
        } else {
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.NOT_LOGIN)).close();
            LogUtils.CONN.error("logout player failure not handshake, playerId={}, session={}", context.getPlayerId(), context);
        }
    }

    public void fastConnect(FastConnectReqMessage message) {
        //从缓存中心查询session
        Profiler.enter("time cost on [query session]");
        ReusableSession session = ReusableSessionManager.I.querySession(message.getSessionIdTmp());
        Profiler.release();
        if (session == null) {
            //1.没查到说明session已经失效了
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.SESSION_EXPIRED)).send();
            LogUtils.CONN.warn("fast connect failure, session is expired, sessionId={}, deviceId={}, conn={}"
                    , message.getSessionIdTmp(), message.getDeviceId(), message.getConnection().getChannel());
        } else if (!session.context.getDeviceId().equals(message.getDeviceId())) {
            //2.非法的设备, 当前设备不是上次生成session时的设备
            ErrorMessage.from(message).setReason(LanguageConfig.getText(LanguageId.INVALID_DEVICE)).send();
            LogUtils.CONN.warn("fast connect failure, not the same device, deviceId={}, session={}, conn={}"
                    , message.getDeviceId(), session.context, message.getConnection().getChannel());
        } else {
            //3.校验成功，重新计算心跳，完成快速重连
            int heartbeat = ConfigManager.I.getHeartbeat();

            session.context.setHeartbeat(heartbeat);
            message.getConnection().setSessionContext(session.context);
            Profiler.enter("time cost on [send FastConnectOkMessage]");
            FastConnectRespMessage
                    .from(message)
                    .setHeartbeat(heartbeat)
                    .sendRaw();
            Profiler.release();
            LogUtils.CONN.info("fast connect success, session={}", session.context);
        }
    }

    public void httpProxy(HttpRequestMessage message) {
        try {
            //1.参数校验
            String method = message.getMethod();
            String uri = message.getUri();
            if (Strings.isNullOrEmpty(uri)) {
                HttpResponseMessage
                        .from(message)
                        .setStatusCode(400)
                        .setReasonPhrase("Bad Request")
                        .sendRaw();
                LogUtils.HTTP.warn("receive bad request url is empty, request={}", message);
            }

            //2.url转换
            uri = doDnsMapping(uri);

            Profiler.enter("time cost on [create FullHttpRequest]");
            //3.包装成HTTP request
            FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.valueOf(method), uri, getBody(message));
            setHeaders(request, message);//处理header

            Profiler.enter("time cost on [HttpClient.request]");
            //4.发送请求
            httpClient.request(new RequestContext(request, new DefaultHttpCallback(message)));
        } catch (Exception e) {
            HttpResponseMessage
                    .from(message)
                    .setStatusCode(502)
                    .setReasonPhrase("Bad Gateway")
                    .sendRaw();
            LOGGER.error("send request ex, message=" + message, e);
            LogUtils.HTTP.error("send proxy request ex, request={}, error={}", message, e.getMessage());
        } finally {
            Profiler.release();
        }
    }

    private static class DefaultHttpCallback implements HttpCallback {
        private final HttpRequestMessage request;
        private int redirectCount;

        private DefaultHttpCallback(HttpRequestMessage request) {
            this.request = request;
        }

        @Override
        public void onResponse(HttpResponse httpResponse) {
            HttpResponseMessage response = HttpResponseMessage
                    .from(request)
                    .setStatusCode(httpResponse.status().code())
                    .setReasonPhrase(httpResponse.status().reasonPhrase());
            for (Map.Entry<String, String> entry : httpResponse.headers()) {
                response.addHeader(entry.getKey(), entry.getValue());
            }

            if (httpResponse instanceof FullHttpResponse) {
                ByteBuf content = ((FullHttpResponse) httpResponse).content();
                if (content != null && content.readableBytes() > 0) {
                    byte[] body = new byte[content.readableBytes()];
                    content.readBytes(body);
                    response.setBody(body);
                    response.addHeader(CONTENT_LENGTH.toString(), Integer.toString(response.getBody().length));
                }
            }
            response.send();
            LogUtils.HTTP.info("send proxy request success end request={}, response={}", request, response);
        }

        @Override
        public void onFailure(int statusCode, String reasonPhrase) {
            HttpResponseMessage
                    .from(request)
                    .setStatusCode(statusCode)
                    .setReasonPhrase(reasonPhrase)
                    .sendRaw();
            LogUtils.HTTP.warn("send proxy request failure end request={}, response={}", request, statusCode + ":" + reasonPhrase);
        }

        @Override
        public void onException(Throwable throwable) {
            HttpResponseMessage
                    .from(request)
                    .setStatusCode(502)
                    .setReasonPhrase("Bad Gateway")
                    .sendRaw();

            LOGGER.error("send proxy request ex end request={}, response={}", request, 502, throwable);
            LogUtils.HTTP.error("send proxy request ex end request={}, response={}, error={}", request, 502, throwable.getMessage());
        }

        @Override
        public void onTimeout() {
            HttpResponseMessage
                    .from(request)
                    .setStatusCode(408)
                    .setReasonPhrase("Request Timeout")
                    .sendRaw();
            LogUtils.HTTP.warn("send proxy request timeout end request={}, response={}", request, 408);
        }

        @Override
        public boolean onRedirect(HttpResponse response) {
            return redirectCount++ < 5;
        }
    }


    private void setHeaders(FullHttpRequest request, HttpRequestMessage message) {
        Map<String, String> headers = message.getHeaders();
        if (headers != null) {
            HttpHeaders httpHeaders = request.headers();
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                httpHeaders.add(entry.getKey(), entry.getValue());
            }
        }

        if (message.getBody() != null && message.getBody().length > 0) {
            request.headers().add(CONTENT_LENGTH, Integer.toString(message.getBody().length));
        }

        InetSocketAddress remoteAddress = (InetSocketAddress) message.getConnection().getChannel().remoteAddress();
        String remoteIp = remoteAddress.getAddress().getHostAddress();//这个要小心，不要使用getHostName,不然会耗时比较大
        request.headers().add("x-forwarded-for", remoteIp);
        request.headers().add("x-forwarded-port", Integer.toString(remoteAddress.getPort()));
    }

    private ByteBuf getBody(HttpRequestMessage message) {
        return message.getBody() == null ? Unpooled.EMPTY_BUFFER : Unpooled.wrappedBuffer(message.getBody());
    }

    private String doDnsMapping(String url) {
        URL uri = null;
        try {
            uri = new URL(url);
        } catch (MalformedURLException e) {
            //ignore e
        }
        if (uri == null) {
            return url;
        }
        String host = uri.getHost();
        DnsMapping mapping = dnsMappingManager.lookup(host);
        if (mapping == null) {
            return url;
        }
        return mapping.translate(uri);
    }

    @Spi(order = 1)
    public static class DefaultBindValidatorFactory implements BindValidatorFactory {
        private final BindValidator validator = playerId -> true;

        @Override
        public BindValidator get() {
            return validator;
        }
    }
}
